---
seotitle: Request validation with Encore.ts
seodesc: Learn how to validation incoming requests with Encore.ts
title: Validation
subtitle: Validate incoming requests
lang: ts
---

When receiving incoming requests it's best practice to validate the payload to ensure it meets your expectations and includes all required fields.

Encore.ts has request validation built in, designed to work seamlessly with TypeScript. It uses the natural TypeScript types directly to validate incoming requests, so you get the best of both worlds: the clean, concise TypeScript syntax, and runtime schema validation. This means your APIs are type-safe during both runtime and compile-time.

Encore.ts makes it easy to define API endpoints that combine data from different sources: some fields from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this:

```ts
import { Header, Query, api } from "encore.dev/api";

interface Request {
  // Optional query parameter. Parsed from the request URL.
  limit?: Query<number>;

  // Custom header that must be set. Parsed from the HTTP headers.
  myHeader: Header<"X-My-Header">;

  // Required enum. Parsed from the request body.
  type: "sprocket" | "widget";
}

export const myEndpoint = api<Request, Response>(
  { expose: true, method: "POST", path: "/api" },
  async ({ limit, myHeader, type }) => {
    // ...
  },
);
```

In the above example, if a request to the endpoint is missing the `X-My-Header` HTTP header or the `type` field in the request body, Encore will return a `400` Bad Request response. The `limit` query parameter is optional and will be either a number or `undefined` in the endpoint handler function.

## Supported validation types

### String fields

```ts
interface Schema {
  name:  string;
}
```

### Number fields

The `number` type will accept both int and float values:

```ts
interface Schema {
  age:  number;
}
```

### Boolean fields

```ts
interface Schema {
  isHuman:  boolean;
}
```

### Array fields

You can define fields wit array values of `string`, `number`, `boolean`, `null`:

```ts
interface Schema {
  strings:  string[];
  numbers:  number[];
  booleans:  boolean[];
  nulls: null[];
}
```

You can have an array of objects:

```ts
interface Schema {
  users:  { name: string; age: number }[];
}
```

It is also possible to have arrays of multiple types:

```ts
interface Schema {
  values:  (string | number)[];
}
```

### Enum fields

String enums can be used in the request schema to validate that a field is one of a set of predefined values. For example:

```ts
interface Schema {
  type:  "BLOG_POST" | "COMMENT"
}
```

You can also use TypeScript enums:

```ts
enum PostType {
  BlogPost = "BLOG_POST",
  Comment = "COMMENT"
}

interface Schema {
  type: PostType,
}
```

The two above examples are equivalent.

### Optional fields

You can make a field optional by adding a `?` after the field name:

```ts
interface Request {
  name?: string;
}

export const myEndpoint = api(
  { expose: true, method: "POST", path: "/body" },
  async (req: Request) => {
    // req.name is a string or undefined
  },
);
```

Request will reach the endpoint handler even if the `name` field is missing. If the field is missing, the value will be `undefined`.

### Nullable fields

You can make a field nullable by using the `| null` type:

```ts
interface Request {
  name: string | null;
}

export const myEndpoint = api(
  { expose: true, method: "POST", path: "/body" },
  async (req: Request) => {
    // req.name is a string or null
  },
);
```

### Union fields

You can define a field that can be one of several types by using a union type:

```ts
interface Request {
  value: string | number | boolean;
}

export const myEndpoint = api(
  { expose: true, method: "POST", path: "/body" },
  async (req: Request) => {
    // req.value is a string, number, or boolean
  },
);
```

### Reference schema

```ts
interface Schema {
  str: string; // String
  int: number; // Number
  list: number[]; // Array of numbers
  listOfTypes: (number | string )[]; // Array multiple types
  nullable: number | null; // Nullable
  maybe?: string; // Optional
  multiple: boolean | number | string | { name: string }; // Union
  enum: "John" | "Foo"; // Enum
}
```

## Value based validation rules

Use Encore.ts's composable value based validation for use cases like checking the format of an email address or the length of a string.
It uses TypeScript's type system and allows you to define validation rules directly in your type definitions.

### Built-in validation Rules
- `Min<N>` / `Max<N>`: Validate minimum/maximum values for numbers
- `MinLen<N>` / `MaxLen<N>`: Validate minimum/maximum lengths for strings and arrays
- `IsURL` / `IsEmail`: Validate that a string is a URL or email address
- `StartsWith` / `EndsWith` / `MatchesRegexp`: Validate that a string matches a specific pattern


### Examples

Import validation rules from the `encore.dev/validate` package:

```ts
import { Min, Max, MinLen, MaxLen, IsEmail, IsURL } from "encore.dev/validate";

interface Schema {
  // Number between 3 and 1000 (inclusive)
  count: number & (Min<3> & Max<1000>);

  // String between 5 and 20 characters
  username: string & (MinLen<5> & MaxLen<20>);

  // Must be either a valid URL or email address
  contact: string & (IsURL | IsEmail);

  // Array of up to 10 email addresses
  recipients: Array<string & IsEmail> & MaxLen<10>;
}
```

### Combining Rules

You can combine multiple validation rules using:
- `&` (and) to require all rules to pass
- `|` (or) to require at least one rule to pass

```ts
interface Schema {
  // Must be both >= 3 and <= 1000
  count: number & (Min<3> & Max<1000>);

  // Must be either a URL or an email
  contact: string & (IsURL | IsEmail);
}
```

### Performance

These validation rules are executed directly in Rust at runtime, before the request reaches your JavaScript code. This provides excellent performance while maintaining type safety at both compile-time and runtime.

## Body

By default, the data is parsed as a JSON body for incoming requests:

```ts
interface Request {
  name: string; // Parsed from the JSON body
}

export const myEndpoint = api<Request, Response>(
  { expose: true, method: "POST", path: "/body" },
  async (req) => {
    // req.name is a string
  },
);
```

Here, `name` is a required field in the request body. If the request body is missing the `name` field, Encore will return a `400` Bad Request response.

## Query

For HTTP methods that support request bodies, parameters are by default read from the HTTP request body as JSON. In those cases, the `Query` type can be used to specify that a field should be parsed from the query string instead.

```typescript
import { api, Query } from "encore.dev/api";

interface Schema {
  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url
}
// A simple API endpoint that echoes the data back.
export const echo = api(
  { method: "POST", path: "/example" },
  async (params: Schema) => {
    // params.query is a string
  },
);
```

This API endpoint expects incoming requests to look like this:
```output
POST /example?query=hello HTTP/1.1
Content-Type: application/json
```

For `GET`, `HEAD` and `DELETE` requests, parameters are read and validated from the query string by default, since those HTTP methods do not support request bodies. For those methods, the `Query` type is not necessary:

```typescript
import { Query } from "encore.dev/api";

interface Schema {
  limit: Query<number>; // always a query parameter
  author: string; // query if GET, HEAD or DELETE, otherwise body parameter
}
```

### Nested query fields

Using the `Query` type as a nested fields has no effect:

```typescript
import { api, Query } from "encore.dev/api";

interface Data {
  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url
  nested: {
    query2: Query<string>; // Query has no effect inside nested fields
  };
}

export const echo = api(
  { method: "POST", path: "/nested" },
  async (params: Data) => {
    // ...
  },
);
```

Nested query params will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this:

```output
POST /nested HTTP/1.1
Content-Type: application/json
{
   "nested": {
      "query2": "not a query string"
   }
}
```

## Headers

Request headers are defined and validated by setting the field type to `Header<"Name-Of-Header">`. It can be used in both request and response data types.

In the example below, the `language` field will be fetched from the `Accept-Language` HTTP header. If the request is missing the `Accept-Language` header, Encore will return a `400` Bad Request response.

```ts
import { Header } from "encore.dev/api";

interface Params {
  language: Header<"Accept-Language">; // parsed from header
  author: string; // not a header
}
```

### Nested header fields

Using the `Header` type as a nested fields has no effect:

```typescript
import { api, Header } from "encore.dev/api";

interface Data {
  header: Header<"X-Header">; // this field will be read from the http header
  nested: {
    header2: Header<"X-Other-Header">; // Header has no effect inside nested fields
  };
}

// A simple API endpoint that echoes the data back.
export const echo = api(
  { method: "POST", path: "/nested" },
  async (params: Data) => {
    // ...
  },
);
```

Nested headers will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this:

```output
POST /nested HTTP/1.1
Content-Type: application/json
X-Header: this is a header
{
   "nested": {
      "header2": "not a header",
   }
}
```

## Params

Dynamic path parameters are also defined in the request schema. The parameter will be parsed from the request URL and made available in the request object:

```ts
import { api } from "encore.dev/api";

interface Request {
  // Required path parameter. Parsed from the request URL.
  id: string;
}

export const myEndpoint = api(
  { expose: true, method: "POST", path: "/user/:id" },
  async ({ id }: Request) => {
    // ...
  },
);
```

You can also use the `number` type for path parameters:

```ts
interface Request {
  id: number;
}
```

Encore.ts will then try to parse the path parameter as a number. If the path parameter is not a valid number, Encore will return a `400` Bad Request response.

## Combining sources

You can combine data from different sources in the same request schema. For example, you can have fields that are parsed from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this:

```ts
import { Header, Query, api } from "encore.dev/api";

interface Request {
  // Required path parameter. Parsed from the request URL.
  id: number;

  // Optional query parameter. Parsed from the request URL.
  limit?: Query<number>;

  // Custom header that must be set. Parsed from the HTTP headers.
  myHeader: Header<"X-My-Header">;

  // Required enum. Parsed from the request body.
  type: "sprocket" | "widget";
}

export const myEndpoint = api(
  { expose: true, method: "POST", path: "/user/:id" },
  async ({ id, limit, myHeader, type }: Request) => {
    // ...
  },
);
```

## Errors

If the validation is not successful, Encore will return a `400` Bad Request response with a JSON body that contains the error message:

```output
HTTP/1.1 400 Bad Request

{
  "code": "invalid_argument",
  "message": "unable to decode request body",
  "internal_message": "Error(\"missing field name\", line: 1, column: 18)"
}
```


## Response

Encore.ts will not perform runtime validation for response data, but you will get compilation errors if you try to return a value that does not match the expected response type.

### Reusing the request type as the response type
You often want to return the same data type that you received in a request. In this case, you can reuse the request type as the response type:

```typescript
import { api, Header, Query } from "encore.dev/api";

interface Data {
  header: Header<"X-Header">; // this field will be read from the http header
  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url
  body: string; // this will be sent as part of the JSON body
}
// A simple API endpoint that echoes the data back.
export const echo = api(
  { method: "POST", path: "/echo" },
  async (params: Data): Promise<Data> => {
    return params; // echo the data back
  },
);
```

This API endpoint expects incoming requests to look like this:
```output
POST /echo?query=hello HTTP/1.1
Content-Type: application/json
X-Header: this is a header
{
   "body": "a body",
}
```

For HTTP responses the `Query<string>` type is considered to be part of the JSON response body, since query strings only make sense for incoming requests.
Responses returned from this endpoint will be serialized as a HTTP response to looks like this:

```output
HTTP/1.1 200 OK
Content-Type: application/json
X-Header: this is a header
{
   "query": "hello",
   "body": "a body",
}
```

## Under the hood

Encore.ts parses your source code to understand the request and response schema that each API endpoint expects, including things like HTTP headers, query parameters, and so on. The schemas are then processed, optimized, and stored as a Protobuf file.

When the Encore.ts Rust runtime starts up, it reads the Protobuf file and pre-computes a request decoder and response encoder, optimized for each API endpoint, using the exact type definition each API endpoint expects. In fact, Encore.ts even handles request validation directly in Rust, ensuring invalid requests never have to even touch the JavaScript layer, mitigating many denial of service attacks.

Encore’s understanding of the request schema also improves performance. JavaScript runtimes like Deno and Bun use a similar architecture (in fact, Deno also uses Rust+Tokio+Hyper), but lack Encore’s understanding of the request schema. As a result, they need to hand over the un-processed HTTP requests to the single-threaded JavaScript engine for execution.

Encore.ts, on the other hand, handles much more of the request processing inside Rust, and only hands over the decoded request objects. By handling much more of the request life-cycle in multi-threaded Rust, the JavaScript event-loop is freed up to focus on executing application business logic instead of parsing HTTP requests, resulting in a significant performance improvement.
